/*
 * BlackLightning.Plugin
 * Andy Tidball
 * 
 * Project: Black Lightning
 * Copyright: GNU General Public License
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.Serialization;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;
using System.Text;
using BlackLightning.PluginLib;
using BlackLightning.PluginLib.Modules;
using BLLib.Configuration;
using BLLib.IO;

namespace BlackLightning {
	/// <summary>
	/// Represents a single plugin which contains one or more modules.  Wraps around an AppDomain and an IPlugin.
	/// </summary>
	internal class Plugin : MarshalByRefObject, IPlugin {
		// constants - options
		private const string OptionCategory                  = "Black Lightning";
		private const string AllowOnNetworkOptionName        = "Allow on Network";
		private const string AllowOnNetworkOptionDescription = "Whether or not the plugin is allowed to use networking to communicate with other computers or servers.";
		private const bool   AllowOnNetworkOptionDefault     = false;

		// member data
		private string                     _Name;     // the plugin's name
		private string                     _Filename; // the filename of the plugin's DLL assembly
		private AppDomain                  _Domain;   // the app domain that all the plugin's code is loaded into
		private PluginProxy                _Proxy;    // the proxy of the plugin that exists in the plugin's app domain
		private Dictionary<string, Module> _Modules;  // the list of modules currently loaded (maps name to object)
		private OptionSet                  _Options;  // the plugin's option set
		private string                     _Folder;   // the folder where the plugin can store its data

		/// <summary>
		/// Creates a new plugin using the given filename.
		/// </summary>
		/// <param name="Filename">The name of the DLL file to load the plugin from.</param>
		public Plugin(string Filename) {
			// make sure this is a DLL file
			if (!Filename.ToLower().EndsWith(".dll")) {
				Filename += ".dll";
			}

			// we need to make sure that the filename matches a DLL file in a case-sensitive fashion
			if (!FileUtil.Exists(Filename, true)) {
				throw new LoadException("File does not exist: " + Filename);
			}

			// save the name and filename
			_Filename = Filename;
			_Name     = Filename.Substring(0, Filename.Length-4);

			// find our data folder
			_Folder = BlackLightning.PreferencesPath + _Name;

			// create our options
			_Options = null;
		}

		/// <summary>
		/// Initializes the plugin by actually loading the code from the external file.
		/// </summary>
		/// <param name="Options">The option set to use for the plugin.</param>
		/// <param name="LoadMessage">The loaded message to send to the plugin once it is loaded.</param>
		public void Initialize(OptionSet Options, LoadedMessage LoadMessage) {
			// save our options
			_Options = Options;
			_Options.IconIndex = BlackLightning.FileIconIndex;
			_Options.SelectedIconIndex = BlackLightning.FileIconIndex;

			// setup the options to let us know when a value changes
			_Options.OptionValueChanged += OptionValueChanged;

			// create a new app domain to load the plugin's functionality into
			_Domain = CreateAppDomain();

			// create a new proxy in the new app domain
			try {
				_Proxy = _Domain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, "BlackLightning.PluginProxy") as PluginProxy;
				_Proxy.Initialize(BlackLightning.ApplicationPath + _Filename);
			} catch (Exception x) {
				if (_Proxy != null) {
					_Proxy.Unload();
				}
				AppDomain.Unload(_Domain);
				throw new LoadException("Unable to load plugin from filename: " + Filename, x);
			}

			// make sure that the plugin's BLVersion is compatible with this version of the program
			// (this can't be done from within PluginProxy because Application.ProductVersion is different in other app domains)
			if (!BlackLightning.IsVersionCompatible(_Proxy.BLVersion)) {
				_Proxy.Unload();
				AppDomain.Unload(_Domain);
				throw new LoadException("The plugin could not be loaded because it is not compatible with the current version of Black Lightning.");
			}

			// create our data folder
			Directory.CreateDirectory(_Folder);

			// wire up the proxy's events to fire ours
			_Proxy.SendMessage += MessageReceived_Plugin;
			_Proxy.SendPacket  += PacketReceived_Plugin;

			// let the plugin know that it was loaded
			if (LoadMessage != null) {
				LoadMessage.DataFolder = _Folder;
				HandleMessage(LoadMessage);
			}

			// initialize our list of modules
			_Modules = new Dictionary<string, Module>();
		}

		/// <summary>
		/// Gets the name of the plugin.
		/// </summary>
		public string Name {
			get {
				return _Name;
			}
		}

		/// <summary>
		/// Gets the filename of the plugin.
		/// </summary>
		public string Filename {
			get {
				return _Filename;
			}
		}

		/// <summary>
		/// Gets the folder where the plugin is allowed to store data files.
		/// </summary>
		public string DataFolder {
			get {
				return _Folder;
			}
		}

		/// <summary>
		/// Gets a list of all the modules currently loaded from the plugin.
		/// </summary>
		public List<string> LoadedModules {
			get {
				string[] Keys = new string[_Modules.Keys.Count];
				_Modules.Keys.CopyTo(Keys, 0);
				List<string> Names = new List<string>(Keys.Length);
				foreach (string Key in Keys) {
					Names.Add(Key);
				}
				return Names;
			}
		}

		/// <summary>
		/// Gets the plugin's option set.
		/// </summary>
		public OptionSet Options {
			get {
				return _Options;
			}
		}

		/// <summary>
		/// Gets the name of the plugin's author, or null if it isn't tagged with one.
		/// </summary>
		public string Author {
			get {
				return _Proxy.Author;
			}
		}

		/// <summary>
		/// Gets the URL of the plugin's web site, or null if it isn't tagged with one.
		/// </summary>
		public string WebSite {
			get {
				return _Proxy.WebSite;
			}
		}

		/// <summary>
		/// Gets the version of the plugin, or null if it isn't tagged with one.
		/// </summary>
		public Version Version {
			get {
				return _Proxy.Version;
			}
		}

		/// <summary>
		/// Gets the version of Black Lightning that this plugin was compiled against.
		/// </summary>
		public Version BLVersion {
			get {
				return _Proxy.BLVersion;
			}
		}

		/// <summary>
		/// Gets the plugin's help text.
		/// </summary>
		public string HelpText {
			get {
				object HelpText = GetProperty(PluginProperty.HelpText);
				if (HelpText == null) {
					return "This plugin does not provide any help text.";
				} else {
					return HelpText as string;
				}
			}
		}

		/// <summary>
		/// Gets whether or not the plugin wishes to receive all packets going to its modules.
		/// </summary>
		public bool ReceiveModulePackets {
			get {
				object ReceiveModulePackets = GetProperty(PluginProperty.ReceiveModulePackets);
				if (ReceiveModulePackets == null) {
					return false;
				} else {
					return (bool)ReceiveModulePackets;
				}
			}
		}

		/// <summary>
		/// Gets whether or not the Plugin wants to receive OptionValueChangedMessages for internal options related to it.
		/// </summary>
		public bool ReceiveInternalOptionUpdates {
			get {
				bool? Receive = GetProperty(PluginProperty.ReceiveInternalOptionUpdates) as bool?;
				if (Receive == null) {
					return false;
				} else {
					return Receive.Value;
				}
			}
		}

		/// <summary>
		/// Fired whenever the underlying plugin or any of its modules fires their SendMessage event.
		/// </summary>
		public event SendMessageEventHandler SendMessage;

		/// <summary>
		/// Fires the SendMessage event.
		/// </summary>
		/// <param name="Sender">The plugin or module that's sending the event.</param>
		/// <param name="e">Arguments to pass to the event.</param>
		private void OnSendMessage(object Sender, SendMessageEventArgs e) {
			if (SendMessage != null) {
				SendMessage(Sender, e);
			}
		}

		/// <summary>
		/// Fired whenever the underlying plugin or any of its modules fires their SendPacket event.
		/// </summary>
		public event SendPacketEventHandler SendPacket;

		/// <summary>
		/// Fires the SendPacket event.
		/// </summary>
		/// <param name="Sender">The plugin or module that's sending the event.</param>
		/// <param name="e">Arguments to pass to the event.</param>
		private void OnSendPacket(object Sender, SendPacketEventArgs e) {
			if (SendPacket != null) {
				SendPacket(Sender, e);
			}
		}

		/// <summary>
		/// Gets the value of the given property from the plugin.
		/// </summary>
		/// <param name="Property">The property whose value to retrieve from the plugin.</param>
		/// <returns>The value of the requested property.</returns>
		public object GetProperty(PluginProperty Property) {
			return _Proxy.GetProperty(Property);
		}

		/// <summary>
		/// Passes the given message down to the underlying plugin.
		/// </summary>
		/// <param name="Message">The message to pass to the underlying plugin.</param>
		public void HandleMessage(Message Message) {
			_Proxy.HandleMessage(Message);
			
			// if this is our loaded message, register our own options
			// we do this here so that the module can't get option value changed messages before their load message
			if (_Options != null && Message is LoadedMessage) {
				_Options.GetOrCreateOption(AllowOnNetworkOptionName).Initialize(new OptionInitializer(AllowOnNetworkOptionDefault.GetType(), AllowOnNetworkOptionDefault, AllowOnNetworkOptionDescription, OptionCategory, BlackLightning.GlobalOptionValueIndex, BlackLightning.UserOptionValueIndex, BlackLightning.CharacterOptionValueIndex));
			}
		}

		/// <summary>
		/// Passes the given packet down to the underlying plugin.
		/// </summary>
		/// <param name="Packet">The packet to pass to the underlying plugin.</param>
		public void HandlePacket(ref BLPacket Packet) {
			_Proxy.HandlePacket(ref Packet);
		}

		/// <summary>
		/// Gets a loaded module object by its name.
		/// </summary>
		/// <param name="ModuleName">The name of the module object to return.</param>
		/// <returns>The module object with the given name, or null if it wasn't found.</returns>
		public Module GetModuleByName(string ModuleName) {
			try {
				return _Modules[ModuleName];
			} catch (KeyNotFoundException) {
				return null;
			}
		}

		/// <summary>
		/// Loads the module with the given name from the plugin.
		/// </summary>
		/// <param name="Name">The name of the module to load.</param>
		/// <param name="LoadMessage">The loaded message to send to the module once it is loaded.</param>
		/// <exception cref="LoadException">Thrown if the module couldn't be loaded.</exception>
		public void LoadModule(string Name, LoadedMessage LoadMessage) {
			// check if this module is already loaded
			if (_Modules.ContainsKey(Name)) {
				throw new LoadException("The module '" + Name + "' is already loaded from plugin: " + this.Name);
			}

			// load the module
			Module M;
			try {
				M = new Module(this, _Proxy.LoadModule(Name));
			} catch (Exception x) {
				throw new LoadException("Unable to load module '" + Name + "' from plugin: " + this.Name, x);
			}

			// wire its events to fire ours
			M.SendMessage += MessageReceived_Module;
			M.SendPacket  += PacketReceived_Module;

			// add it to our list
			_Modules.Add(M.Name, M);

			// send it a loaded message
			LoadMessage.DataFolder = _Folder;
			M.HandleMessage(LoadMessage);
		}

		/// <summary>
		/// Unloads the module with the given name from the plugin.
		/// </summary>
		/// <param name="Name">The name of the module to unload.</param>
		/// <exception cref="LoadException">Thrown if the module couldn't be unloaded.</exception>
		public void UnloadModule(string Name) {
			// find the module
			Module M = null;
			try {
				M = _Modules[Name];
			} catch (KeyNotFoundException x) {
				throw new LoadException("No module named '" + Name + "' currently loaded in plugin: " + _Name, x);
			}

			// send it an unloaded message
			M.HandleMessage(new UnloadedMessage());

			// remove it from our list
			_Modules.Remove(M.Name);

			// unwire its events from ours
			M.SendMessage -= MessageReceived_Module;
			M.SendPacket  -= PacketReceived_Module;

			// unload it
			M.Unload();
		}

		/// <summary>
		/// Unloads the plugin.
		/// </summary>
		/// <param name="MessageHandler">The handler to unwire from the plugin's SendMessage event.</param>
		/// <param name="PacketHandler">The handler to unwire from the plugin's SendPacket event.</param>
		public void Unload(SendMessageEventHandler MessageHandler, SendPacketEventHandler PacketHandler) {
			// unload any modules that are loaded
			foreach (string ModuleName in LoadedModules) {
				UnloadModule(ModuleName);
			}

			// let the plugin know that we're being unloaded
			HandleMessage(new UnloadedMessage());

			// unwire our events from the given handlers
			if (MessageHandler != null) {
				this.SendMessage -= MessageHandler;
			}
			if (PacketHandler != null) {
				this.SendPacket  -= PacketHandler;
			}

			// unwire its events from ours
			_Proxy.SendMessage -= MessageReceived_Plugin;
			_Proxy.SendPacket  -= PacketReceived_Plugin;

			// stop listening for option value changes and uninitialize all of our options
			_Options.OptionValueChanged -= OptionValueChanged;
			_Options.UninitiazeAll(false);
			_Options = null;

			// unload the proxy
			_Proxy.Unload();
			_Proxy = null;

			// unload the app domain
			AppDomain.Unload(_Domain);
			_Domain = null;
		}

		/// <summary>
		/// Scans the plugin for a list of modules.
		/// </summary>
		/// <returns>A list of modules in the plugin.</returns>
		public string[] ScanForModules() {
			return _Proxy.ScanForModules();
		}

		/// <summary>
		/// Validates the module with the given name to make sure it's compatible with IModule.
		/// </summary>
		/// <param name="Name">The name of the module to validate.</param>
		public void ValidateModule(string Name) {
			_Proxy.ValidateModule(Name);
		}

		/// <summary>
		/// Called when the LeaseManager needs to acquire a Lease for how long we'll live on by ourselves.
		/// </summary>
		/// <returns>A Lease that will be used to control our lifetime.</returns>
		public override object InitializeLifetimeService() {
			// return null to signal an infinite amount of time
			return null;
		}

		/// <summary>
		/// Creates an app domain to load the external functionality of the plugin into.
		/// </summary>
		/// <returns>The app domain to use to load external functionality into.</returns>
		private AppDomain CreateAppDomain() {
			// add our application directory to the host evidence for the app domain
			Evidence E = new Evidence(AppDomain.CurrentDomain.Evidence);
			E.AddHost(new ApplicationDirectory(BlackLightning.ApplicationPath));

			// create a new app domain with the evidence
			AppDomain Domain = AppDomain.CreateDomain(_Name, E);

			// start with our default permissions
			PermissionSet PS = new PermissionSet(BlackLightning.DefaultPermissions);

			// add file access permissions to our folder
			PS.AddPermission(new FileIOPermission(FileIOPermissionAccess.AllAccess, _Folder));

			// check if we have network access
			bool NetworkAllowed = false;
			Option O = _Options.GetOption(Plugin.AllowOnNetworkOptionName);
			if (O != null && O.Value as string == true.ToString()) {
				NetworkAllowed = true;
			}

			// add network permissions if the user has allowed them
			if (NetworkAllowed) {
				PS.AddPermission(new DnsPermission(PermissionState.Unrestricted));
				PS.AddPermission(new SocketPermission(NetworkAccess.Connect, TransportType.All, "*.*.*.*", SocketPermission.AllPorts));
			}

			// set the domain's security policy to include the permissions we created
			Domain.SetAppDomainPolicy(BlackLightning.CreateSecurityPolicy(PS));

			// return the new app domain
			return Domain;
		}

		/// <summary>
		/// Handles the underlying plugin's SendMessage event.
		/// </summary>
		/// <param name="Sender">The underlying plugin.</param>
		/// <param name="e">Arguments about the event.</param>
		private void MessageReceived_Plugin(object Sender, SendMessageEventArgs e) {
			if (e.Message is RegisterOptionMessage) {
				// this is an option registration, we can handle that
				MessageReceived_Plugin_RegisterOption(e.Message as RegisterOptionMessage);
			} else if (e.Message is UnregisterOptionMessage) {
				// this is an option unregistration, we can handle that
				MessageReceived_Plugin_UnregisterOption(e.Message as UnregisterOptionMessage);
			} else if (e.Message is ChangeOptionValueMessage) {
				// they want to change an option's value, we can handle that
				MessageReceived_Plugin_ChangeOptionValue(e.Message as ChangeOptionValueMessage);
			} else {
				// fire the event further upwards
				OnSendMessage(this, e);
			}
		}

		/// <summary>
		/// Handles the underlying plugin sending a RegisterOptionMessage.
		/// </summary>
		/// <param name="Message">The message sent by the underlying plugin.</param>
		private void MessageReceived_Plugin_RegisterOption(RegisterOptionMessage Message) {
			// make sure they're not using the same category we use for our own options
			if (Message.Category == OptionCategory) {
				HandleMessage(new ErrorMessage(Message, new InvalidOperationException("The category '" + OptionCategory + "' is reserved for internal use.")));
				return;
			}

			// initialize the option as per the message
			Option O = _Options.GetOrCreateOption(Message.Name);
			O.Initialize(Message.GetInitializer(new int[]{BlackLightning.GlobalOptionValueIndex, BlackLightning.UserOptionValueIndex, BlackLightning.CharacterOptionValueIndex}));
		}

		/// <summary>
		/// Handles the underlying plugin sending a RegisterOptionMessage.
		/// </summary>
		/// <param name="Message">The message sent by the underlying plugin.</param>
		private void MessageReceived_Plugin_UnregisterOption(UnregisterOptionMessage Message) {
			// make sure that we have an option by that name
			if (!_Options.HasOption(Message.Name)) {
				// send an error message to the plugin
				HandleMessage(new ErrorMessage(Message, new ArgumentException("There is no registered option with the given name.")));
				return;
			}

			// remove the option
			_Options.RemoveOption(Message.Name);
		}

		/// <summary>
		/// Handles the underlying plugin sending a ChangeOptionValueMessage.
		/// </summary>
		/// <param name="Message">The message sent by the underlying plugin.</param>
		private void MessageReceived_Plugin_ChangeOptionValue(ChangeOptionValueMessage Message) {
			// make sure that we actually have an option by that name
			if (!_Options.HasOption(Message.Name)) {
				// send an error message to the plugin
				HandleMessage(new ErrorMessage(Message, new ArgumentException("There is no registered option with the given name.")));
				return;
			}

			try {
				// grab the option and make sure it's not one of ours
				Option O = _Options.GetOption(Message.Name);
				if (O.Category == OptionCategory) {
					throw new InvalidOperationException("Options in the category '" + OptionCategory + "' are reserved for internal use.");
				}

				// set the option's value
				O.Value = Message.Value;
				O.ApplyNewValue();
			} catch (Exception x) {
				// send an error message to the plugin
				HandleMessage(new ErrorMessage(Message, x));
			}
		}

		/// <summary>
		/// Handles the underlying plugin's SendPacket event.
		/// </summary>
		/// <param name="Sender">The underlying plugin.</param>
		/// <param name="e">Arguments about the event.</param>
		private void PacketReceived_Plugin(object Sender, SendPacketEventArgs e) {
			// fire our own SendPacket event
			OnSendPacket(this, e);
		}

		/// <summary>
		/// Handles the SendMessage event for all modules loaded in this plugin.
		/// </summary>
		/// <param name="Sender">The underlying plugin.</param>
		/// <param name="e">Arguments about the event.</param>
		private void MessageReceived_Module(object Sender, SendMessageEventArgs e) {
			// fire our own SendMessage event
			OnSendMessage(Sender, e);
		}

		/// <summary>
		/// Handles the SendPacket event for all modules loaded in this plugin.
		/// </summary>
		/// <param name="Sender">The underlying plugin.</param>
		/// <param name="e">Arguments about the event.</param>
		private void PacketReceived_Module(object Sender, SendPacketEventArgs e) {
			// fire our own SendPacket event
			OnSendPacket(Sender, e);
		}

		/// <summary>
		/// Handles any of the plugin's option's value changing.
		/// </summary>
		/// <param name="Sender">The option whose value changed.</param>
		/// <param name="e">Arguments about the event.</param>
		private void OptionValueChanged(object Sender, OptionValueChangedEventArgs e) {
			// unless this is a change in the lowest option level, we don't care
			Option O = Sender as Option;
			if (e.ValueIndex != O.LowestValueIndex) {
				return;
			}

			// check if this is one of our internal options
			if (O.Category == OptionCategory) {
				// if they don't want these messages, return now
				if (!ReceiveInternalOptionUpdates) {
					return;
				}
			}

			// let the plugin know that an option value changed
			HandleMessage(new OptionValueChangedMessage(O.Name, e.OldValue, e.NewValue));
		}
	}

	/// <summary>
	/// Represents a loaded plugin in its own app domain.
	/// </summary>
	internal class PluginProxy : MarshalByRefObject, IPlugin {
		// member data
		private Assembly                 _Assembly;    // the assembly that the external functionality resides in
		private IPlugin                  _Plugin;      // the external plugin's functionality
		private Dictionary<string, Type> _ModuleTypes; // maps module names to their corresponding types
		private Version                  _BLVersion;   // the version of Black Lightning that the plugin was compiled against

		/// <summary>
		/// Creates a new PluginProxy.
		/// </summary>
		public PluginProxy() {
		}

		/// <summary>
		/// Loads up the external functionality from the assembly and loads a plugin type from it if there is one.
		/// </summary>
		/// <param name="AssemblyName">The name of the assembly file to load.</param>
		public void Initialize(string AssemblyName) {
			// load up the assembly
			_Assembly = Assembly.LoadFrom(AssemblyName);

			// check for the BLVersion attribute
			object[] VersionAttributes = _Assembly.GetCustomAttributes(typeof(BLVersionAttribute), true);
			if (VersionAttributes.Length != 1) {
				throw new LoadException("The assembly '" + AssemblyName + "' is not tagged with a BLVersion attribute.");
			}
			_BLVersion = new Version((VersionAttributes[0] as BLVersionAttribute).Version);

			// find all the module types
			_ModuleTypes = new Dictionary<string,Type>();
			object[] ModuleAttributes = _Assembly.GetCustomAttributes(typeof(BLModuleAttribute), true);
			foreach (BLModuleAttribute A in ModuleAttributes) {
				// find the name of this type
				string Name = A.Type.Name;

				// check if its type name is overridden by an attribute
				object[] NameAttributes = A.Type.GetCustomAttributes(typeof(ModuleNameAttribute), true);
				if (NameAttributes.Length > 0) {
					Name = ((ModuleNameAttribute)NameAttributes[0]).Name;
				}

				// add the name -> type mapping to our list
				_ModuleTypes.Add(Name, A.Type);
			}

			// load the plugin class from the assembly
			_Plugin = LoadPlugin();
			if (_Plugin != null) {
				// wire up the plugin's events to fire ours
				_Plugin.SendMessage += MessageReceived;
				_Plugin.SendPacket  += PacketReceived;
			}
		}

		/// <summary>
		/// Gets the version of the plugin's assembly, or null if it isn't tagged.
		/// </summary>
		public Version Version {
			get {
				return _Assembly.GetName().Version;
			}
		}

		/// <summary>
		/// Gets the name of the author of the plugin, or null if it isn't tagged.
		/// </summary>
		public string Author {
			get {
				object[] Attributes = _Assembly.GetCustomAttributes(typeof(AuthorAttribute), true);
				if (Attributes.Length == 0) {
					return null;
				} else {
					return ((AuthorAttribute)Attributes[0]).Name;
				}
			}
		}

		/// <summary>
		/// Gets the plugin's related web site, or null if it doesn't have one.
		/// </summary>
		public string WebSite {
			get {
				object[] Attributes = _Assembly.GetCustomAttributes(typeof(WebSiteAttribute), true);
				if (Attributes.Length == 0) {
					return null;
				} else {
					return ((WebSiteAttribute)Attributes[0]).URL;
				}
			}
		}

		/// <summary>
		/// Gets the version of Black Lightning that this plugin was compiled against.
		/// </summary>
		public Version BLVersion {
			get {
				return _BLVersion;
			}
		}
		
		/// <summary>
		/// Fired whenever the underlying plugin fires its SendMessage event.
		/// </summary>
		public event SendMessageEventHandler SendMessage;

		/// <summary>
		/// Fires the SendMessage event.
		/// </summary>
		/// <param name="e">Arguments to pass to the event.</param>
		private void OnSendMessage(SendMessageEventArgs e) {
			if (SendMessage != null) {
				SendMessage(this, e);
			}
		}

		/// <summary>
		/// Fired whenever the underlying plugin fires its SendPacket event.
		/// </summary>
		public event SendPacketEventHandler SendPacket;

		/// <summary>
		/// Fires the SendPacket event.
		/// </summary>
		/// <param name="e">Arguments to pass to the event.</param>
		private void OnSendPacket(SendPacketEventArgs e) {
			if (SendPacket != null) {
				SendPacket(this, e);
			}
		}

		/// <summary>
		/// Gets the value of the given property from the plugin.
		/// </summary>
		/// <param name="Property">The property whose value to retrieve from the plugin.</param>
		/// <returns>The value of the requested property.</returns>
		public object GetProperty(PluginProperty Property) {
			if (_Plugin == null) {
				return null;
			}

			try {
				return _Plugin.GetProperty(Property);
			} catch (Exception x) {
				OnSendPacket(new SendPacketEventArgs(new ExceptionPacket(x)));
				return null;
			}
		}

		/// <summary>
		/// Passes the given message down to the underlying plugin.
		/// </summary>
		/// <param name="Message">The message to pass to the underlying plugin.</param>
		public void HandleMessage(Message Message) {
			if (_Plugin != null) {
				try {
					_Plugin.HandleMessage(Message);
				} catch (Exception x) {
					OnSendPacket(new SendPacketEventArgs(new ExceptionPacket(x)));
				}
			}
		}

		/// <summary>
		/// Passes the given packet down to the underlying plugin.
		/// </summary>
		/// <param name="Packet">The packet to pass to the underlying plugin.</param>
		public void HandlePacket(ref BLPacket Packet) {
			if (_Plugin != null) {
				try {
					_Plugin.HandlePacket(ref Packet);
				} catch (Exception x) {
					OnSendPacket(new SendPacketEventArgs(new ExceptionPacket(x)));
				}
			}
		}

		/// <summary>
		/// Loads the module with the given name from the plugin.
		/// </summary>
		/// <param name="Name">The name of the module to load.</param>
		/// <returns>The loaded module.</returns>
		/// <exception cref="LoadException">Thrown if the module cannot be loaded.</exception>
		public ModuleProxy LoadModule(string Name) {
			Type T = null;
			try {
				// validate the module
				ValidateModule(Name);

				// find the type for this name
				T = _ModuleTypes[Name];

				// create an instance of the type
				// the type has already been validated to contain a default constructor
				object Instance = T.GetConstructor(new Type[0]).Invoke(new Object[0]);
				if (!(Instance is IModule)) {
					// this should never happen, it should be impossible for ModuleType to not be a type derived from IModule
					throw new LoadException("Constructing an instance of type '" + T.Name + "' resulted in a type which is not derived from IModule.");
				}

				// wrap the IModule in a ModuleProxy and return it
				return new ModuleProxy(Instance as IModule);
			} catch (LoadException) {
				throw;
			} catch (KeyNotFoundException x) {
				throw new LoadException("No module named '" + Name + "' exists in this plugin.", x);
			} catch (System.Reflection.TargetInvocationException x) {
				throw new LoadException("The module '" + Name + "' threw an exception during construction.", x);
			}
		}
/*
		/// <summary>
		/// Unloads the given module from the plugin.
		/// </summary>
		/// <param name="Module">The module to unload from the plugin.</param>
		public void UnloadModule(ModuleProxy Module) {
			// unload the module
			Module.Unload();
		}
*/
		/// <summary>
		/// Unloads the plugin.
		/// </summary>
		public void Unload() {
			// if there's a plugin, unwire its events from ours
			if (_Plugin != null) {
				_Plugin.SendMessage -= MessageReceived;
				_Plugin.SendPacket  -= PacketReceived;
			}

			// free our references
			_Assembly = null;
			_Plugin   = null;
			if (_ModuleTypes != null) {
				_ModuleTypes.Clear();
				_ModuleTypes = null;
			}
		}

		/// <summary>
		/// Scans the plugin for all of its module names.
		/// </summary>
		/// <returns>A list of names of all the modules in the plugin.</returns>
		public string[] ScanForModules() {
			string[] Keys = new string[_ModuleTypes.Keys.Count];
			_ModuleTypes.Keys.CopyTo(Keys, 0);
			return Keys;
		}

		/// <summary>
		/// Validates the given module to make sure it can be loaded.
		/// </summary>
		/// <param name="Name">The name of the module to validate.</param>
		public void ValidateModule(string Name) {
			// find the type corresponding to this module
			Type T;
			try {
				T = _ModuleTypes[Name];
			} catch (KeyNotFoundException x) {
				throw new LoadException("No module named '" + Name + "' exists in this plugin.", x);
			}

			// check if this type implements the module interface
			Type[] Interfaces = T.FindInterfaces(System.Reflection.Module.FilterTypeName, typeof(IModule).Name);
			if (Interfaces.Length < 1) {
				// the type doesn't even claim to implement the interface
				throw new LoadException("The type '" + T.Name + "' does not implement the IModule interface.");
			}
			if (Interfaces.Length > 1) {
				// the type doesn't even claim to implement the interface
				throw new LoadException("The type '" + T.Name + "' appears to implement the IModule interface more than once.");
			}

			// make sure the type has a default constructor
			ConstructorInfo Constructor = T.GetConstructor(new Type[0]);
			if ((Constructor == null) || (Constructor.IsAbstract) || (!Constructor.IsPublic) || (Constructor.IsStatic)) {
				throw new LoadException("The type '" + T.Name + "' does not support a default constructor.");
			}
		}

		/// <summary>
		/// Called when the LeaseManager needs to acquire a Lease for how long we'll live on by ourselves.
		/// </summary>
		/// <returns>A Lease that will be used to control our lifetime.</returns>
		public override object InitializeLifetimeService() {
			// return null to signal an infinite amount of time
			return null;
		}

		/// <summary>
		/// If the assembly contains a tag for an IPlugin type, then it is loaded and returned.
		/// </summary>
		/// <returns>An IPlugin for the loaded type or null if none is tagged.</returns>
		/// <exception cref="LoadException">Thrown if the type cannot be loaded.</exception>
		private IPlugin LoadPlugin() {
			// find the type for the plugin
			Type T = ScanForPlugin();
			if (T == null) {
				return null;
			}

			// validate the type
			ValidatePlugin(T);

			// create an instance of the type
			try {
				// the type has already been validated to contain a default constructor
				object Instance = T.GetConstructor(new Type[0]).Invoke(new Object[0]);
				if (!(Instance is IPlugin)) {
					// this should never happen, it should be impossible for T to not be a type derived from IPlugin
					throw new LoadException("Constructing an instance of type '" + T.Name + "' resulted in a type which is not derived from IPlugin");
				}
				return Instance as IPlugin;
			} catch (System.Reflection.TargetInvocationException x) {
				throw new LoadException("The plugin threw an exception during construction.", x);
			}
		}

		/// <summary>
		/// Retrieves the type in the assembly which is tagged as a plugin.
		/// </summary>
		/// <returns>The type which is tagged as a plugin, or null if none is.</returns>
		private Type ScanForPlugin() {
			object[] Attributes = _Assembly.GetCustomAttributes(typeof(BLPluginAttribute), true);
			if (Attributes.Length == 0) {
				return null;
			} else {
				return ((BLPluginAttribute)Attributes[0]).Type;
			}
		}

		/// <summary>
		/// Validates the given type as an IPlugin.
		/// </summary>
		/// <param name="T">The type to validate as an IPlugin.</param>
		/// <exception cref="LoadException">Thrown if the type fails to validate.</exception>
		private void ValidatePlugin(Type T) {
			// check if this type implements the plugin interface
			Type[] Interfaces = T.FindInterfaces(System.Reflection.Module.FilterTypeName, typeof(IPlugin).Name);
			if (Interfaces.Length < 1) {
				// the type doesn't even claim to implement the interface
				throw new LoadException("The type '" + T.Name + "' does not implement the IPlugin interface.");
			}
			if (Interfaces.Length > 1) {
				// the type doesn't even claim to implement the interface
				throw new LoadException("The type '" + T.Name + "' appears to implement the IPlugin interface more than once.");
			}

			// make sure the type has a default constructor
			ConstructorInfo Constructor = T.GetConstructor(new Type[0]);
			if ((Constructor == null) || (Constructor.IsAbstract) || (!Constructor.IsPublic) || (Constructor.IsStatic)) {
				throw new LoadException("The type '" + T.Name + "' does not support a default constructor.");
			}
		}

		/// <summary>
		/// Handles the underlying plugin's SendMessage event.
		/// </summary>
		/// <param name="Sender">The underlying plugin.</param>
		/// <param name="e">Arguments about the event.</param>
		private void MessageReceived(object Sender, SendMessageEventArgs e) {
			// fire our own SendMessage event
			OnSendMessage(e);
		}

		/// <summary>
		/// Handles the underlying plugin's SendPacket event.
		/// </summary>
		/// <param name="Sender">The underlying plugin.</param>
		/// <param name="e">Arguments about the event.</param>
		private void PacketReceived(object Sender, SendPacketEventArgs e) {
			// fire our own SendPacket event
			OnSendPacket(e);
		}
	}

	/// <summary>
	/// A set of information about a plugin that hasn't actually been loaded.
	/// </summary>
	internal class PluginInfo {
		// member data
		private string                            _Name;      // the name of the plugin
		private Version                           _Version;   // the version of the plugin
		private string                            _Author;    // the name of the plugin's author
		private string                            _WebSite;   // the URL of the author's web site, or null if they don't have one
		private Version                           _BLVersion; // the earliest version of Black Lightning that the plugin is compatible with
		private Dictionary<string, LoadException> _Modules;   // a list of modules in the plugin (maps a module's name to the exception thrown on validating it)

		/// <summary>
		/// Creates a new PluginInfo.
		/// </summary>
		/// <param name="Filename">The filename of the plugin to retrieve information about.</param>
		public PluginInfo(string Filename) {
			// load the plugin
			Plugin P = new Plugin(Filename);
			P.Initialize(new OptionSet("Temporary"), null);

			// record all the information we need
			_Name = P.Name;
			_Version = P.Version;
			_Author  = P.Author;
			_WebSite = P.WebSite;
			_BLVersion = P.BLVersion;
			
			// run through each module and validate it
			_Modules = new Dictionary<string,LoadException>();
			foreach (string ModuleName in P.ScanForModules()) {
				try {
					P.ValidateModule(ModuleName);
					_Modules.Add(ModuleName, null);
				} catch (LoadException x) {
					_Modules.Add(ModuleName, x);
				}
			}

			// got everything, unload the plugin
			P.Unload(null, null);
		}

		/// <summary>
		/// The name of the plugin.
		/// </summary>
		public string Name {
			get {
				return _Name;
			}
		}

		/// <summary>
		/// The plugin's assembly version, or null if it doesn't have one.
		/// </summary>
		public Version Version {
			get {
				return _Version;
			}
		}

		/// <summary>
		/// The name of the plugin's author, or null if it wasn't marked with one.
		/// </summary>
		public string Author {
			get {
				return _Author;
			}
		}

		/// <summary>
		/// The URL of the plugin's related web site, or null if it wasn't marked with one.
		/// </summary>
		public string WebSite {
			get {
				return _WebSite;
			}
		}

		/// <summary>
		/// The earliest version of Black Lightning that the plugin is compatible with.
		/// </summary>
		public Version BLVersion {
			get {
				return _BLVersion;
			}
		}

		/// <summary>
		/// A list of modules in the plugin mapped to the exceptions thrown when they were validated (null if they validated fine).
		/// </summary>
		public Dictionary<string, LoadException> Modules {
			get {
				return _Modules;
			}
		}

		/// <summary>
		/// Formats all the available information for display in the front end.
		/// </summary>
		/// <returns>A string formatted for display in the front end.</returns>
		public string FormatForDisplay() {
			// calculate the version that will be incompatibile with the plugin
			Version HighVersion = new Version(BLVersion.Major+1, 0, 0, 0);

			// start with the easy stuff
			StringBuilder Output = new StringBuilder();
			Output.Append("Name: "          + Name                                                           + Environment.NewLine);
			Output.Append("Version: "       + (Version == null ? "Unknown" : Version.ToString())             + Environment.NewLine);
			Output.Append("Author: "        + (Author  == null ? "Unknown" : Author)                         + Environment.NewLine);
			Output.Append("Web Site: "      + (WebSite == null ? "None"    : WebSite)                        + Environment.NewLine);
			Output.Append("Compatibility: " + BLVersion.ToString() + " - " + HighVersion.ToString()          + Environment.NewLine);
			Output.Append("Compatible: "    + (BlackLightning.IsVersionCompatible(BLVersion) ? "Yes" : "No") + Environment.NewLine);

			// run through each module
			Output.Append("Modules:" + Environment.NewLine);
			foreach (KeyValuePair<string, LoadException> Module in Modules) {
				Output.Append("    ");
				Output.Append(Module.Key);
				if (Module.Value != null) {
					Output.Append(" (Validation Failed: ");
					Output.Append(Module.Value.Message);
					Output.Append(")");
				}
				Output.Append(Environment.NewLine);
			}

			return Output.ToString();
		}
	}

	/// <summary>
	/// An exception that occurs when loading a module type.
	/// </summary>
	[Serializable]
	internal class LoadException : Exception {
		/// <summary>
		/// Creates a new LoadException.
		/// </summary>
		/// <param name="Message">The reason that the loading failed.</param>
		public LoadException(string Message)
			: base(Message) {
		}

		/// <summary>
		/// Creates a new LoadException.
		/// </summary>
		/// <param name="Message">The reason that the loading failed.</param>
		/// <param name="InnerException">The exception that caused this one.</param>
		public LoadException(string Message, Exception InnerException)
			: base(Message, InnerException) {
		}

		/// <summary>
		/// Deserializes a LoadException.
		/// </summary>
		/// <param name="Info">Serialization information?</param>
		/// <param name="Context">The context that it's being deserialized into?</param>
		public LoadException(SerializationInfo Info, StreamingContext Context)
			: base(Info, Context) {
		}
	}
}